fix(cli): align search & verify table output (#21)#22
Conversation
`skillrig search` printed name/version/description with single-space separators and no padding, so columns were ragged and unreadable (#21). Render both the search list and the verify failure list through a shared text/tabwriter helper (stdlib, no new dependency) so columns align across rows. Also make truncateDesc rune-safe: byte-slicing could split a multibyte rune into invalid UTF-8, and rune width matches how tabwriter measures cell width. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep the intentional CLI output glyphs (the → next-step hint, ✓/✗ verify status, the —/·/… separators) but guard against ACCIDENTAL new non-ASCII runes (copy-pasted smart quotes, non-breaking spaces, homoglyphs) that render unpredictably and are nearly invisible in review. golangci-lint can't catch these: asciicheck only inspects identifiers, bidichk only catches dangerous invisible/bidi runes. - internal/sourceguard: a test-only package that scans every .go file and fails on any non-ASCII rune outside a documented allowlist, reporting file:line:col. Runs via 'go test ./...', so 'make check' and CI enforce it for free — no workflow edit. - .claude/settings.json: a PostToolUse hook (Edit|Write|MultiEdit) that runs the guard after .go edits and blocks with exit 2 on a violation, feeding the offending location back — the agent-loop guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review Summary by Qodo(Agentic_describe updated until commit 0d66d94)Fix table alignment and add non-ASCII source guard
WalkthroughsDescription• Fix ragged table output in search and verify commands using stdlib text/tabwriter • Make truncateDesc rune-safe to prevent UTF-8 corruption from multibyte character splitting • Add internal/sourceguard test package to guard against accidental non-ASCII runes in source • Add Claude agent-loop hook in .claude/settings.json to enforce ASCII guard on .go edits Diagramflowchart LR
A["search/verify output"] -->|"ragged columns"| B["renderSearchResult<br/>renderVerifyFailure"]
B -->|"new: writeAlignedColumns"| C["text/tabwriter<br/>elastic tabstops"]
C -->|"aligned output"| D["fixed-width columns"]
E["truncateDesc<br/>byte-slicing"] -->|"UTF-8 corruption risk"| F["rune-safe conversion"]
F -->|"rune counting"| G["matches tabwriter width"]
H[".go source edits"] -->|"PostToolUse hook"| I["sourceguard test"]
I -->|"sanctioned allowlist"| J["intentional glyphs only"]
File Changes1. internal/cli/output.go
|
Code Review by Qodo
Context used✅ Tickets:
🎫 Bug: Tables are unreadable in CLI✅ Compliance rules (platform):
152 rules 1. tabwriter.NewWriter args not multiline
|
🤖 I have created a release *beep* *boop* --- ## [1.0.1](v1.0.0...v1.0.1) (2026-06-02) ### Bug Fixes * add MIT license ([#14](#14)) ([346bce0](346bce0)) * **cli:** align search & verify table output ([#21](#21)) ([#22](#22)) ([b54d751](b54d751)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: skillrig-release-bot[bot] <289564490+skillrig-release-bot[bot]@users.noreply.github.com>
writeAlignedColumns joined cells with \t and fed them to text/tabwriter, so a tab inside any cell (catalog description or skill name/reason, taken verbatim and not guaranteed tab-free) was read as an extra column separator and corrupted the alignment this slice added. Sanitize \t/\n/\r to spaces in each cell before joining, with a unit test proving a tabbed cell can no longer inject a column. Also break tabwriter.NewWriter onto one argument per line (style: 4+ args), per the Qodo review on PR #22. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The PostToolUse hook embedded a long multi-statement shell command on a single JSON line (>120 chars, unreviewable). Move the logic into .claude/hooks/ascii-guard.sh and reference it from .claude/settings.json, so the guard is readable, executable, and independently testable. Behavior is unchanged (clean .go -> 0, non-go -> 0, violation -> exit 2). Per the Qodo review on PR #22. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
writeAlignedColumns joined cells with \t and fed them to text/tabwriter, so a tab inside any cell (catalog description or skill name/reason, taken verbatim and not guaranteed tab-free) was read as an extra column separator and corrupted the alignment this slice added. Sanitize \t/\n/\r to spaces in each cell before joining, with a unit test proving a tabbed cell can no longer inject a column. Also break tabwriter.NewWriter onto one argument per line (style: 4+ args), per the Qodo review on PR #22. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The PostToolUse hook embedded a long multi-statement shell command on a single JSON line (>120 chars, unreviewable). Move the logic into .claude/hooks/ascii-guard.sh and reference it from .claude/settings.json, so the guard is readable, executable, and independently testable. Behavior is unchanged (clean .go -> 0, non-go -> 0, violation -> exit 2). Per the Qodo review on PR #22. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to #22, addressing the Qodo review on that PR. ## Summary - **🐞 Bug — tabs corrupt the table** (`writeAlignedColumns`). Cells were `\t`-joined and fed to `text/tabwriter`, so a tab inside any cell (catalog description or skill name/reason — taken verbatim, not guaranteed tab-free) was read as an extra column separator and broke alignment. Now each cell is sanitized (`\t`/`\n`/`\r` → space) before joining, with a unit test proving a tabbed cell can't inject a column. - **Style — `tabwriter.NewWriter` args** broken onto one-arg-per-line (rule 782553 / our own `golang-code-style`). - **Maintainability — hook command extracted to a script.** The `PostToolUse` hook's long single-line shell command (>120 chars, rule 782552) now lives in `.claude/hooks/ascii-guard.sh`, referenced from `.claude/settings.json`. Behavior unchanged. ## Not included (deliberate) - Qodo flagged `truncateDesc` using `…` instead of ASCII `...` (rule 783450). That conflicts with the maintainer's decision to keep the intentional output glyphs, and changing an org-wide rule vs. complying is a pending governance call — left out of this PR. ## Test plan - `make check` green: gofmt, `go vet`, golangci-lint **0 issues**, all tests (incl. integration). - New `TestWriteAlignedColumns_NeutralizesTabsInCells` passes. - Extracted hook script re-pipe-tested: clean `.go` → exit 0, non-Go → exit 0 (skipped), injected `U+201C` → exit 2 with location on stderr. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Fixes #21 —
skillrig searchprintedname version — descriptionwith single-space separators and no column padding, so every row's columns started at a different offset and the list was unreadable. This renders the search list (and the verify failure list, which had the same defect) through a sharedtext/tabwriterhelper — stdlib elastic tabstops, no new dependency (confirmed none of go-toml/cobra/yaml provide table formatting, and Cobra has no fixed-width helper).Changes
internal/cli/output.gowriteAlignedColumnshelper (one shared tabwriter renderer for bothsearchand theverifyfailure list — no duplicated setup).truncateDescis now rune-safe: the olds[:n]byte-slice could split a multibyte rune into invalid UTF-8, and rune width matches how tabwriter measures cell width.→ ✓ ✗ — · …but guard against accidental new non-ASCII — smart quotes, NBSP, homoglyphs — which golangci-lint can't catch:asciicheckis identifiers-only,bidichkis dangerous-invisibles-only):internal/sourceguard/— a test-only package that scans every.gofile and fails on any non-ASCII rune outside a documented allowlist (file:line:col+ codepoint). Runs viago test ./..., somake checkand CI enforce it for free — no workflow edit..claude/settings.json— aPostToolUsehook (Edit|Write|MultiEdit) that runs the guard after.goedits and blocks (exit 2) on a violation, feeding the location back. Validated end-to-end (caught an injectedU+2019).Before / after (
skillrig search)Testing
make check— green: gofmt clean,go vetclean, golangci-lint 0 issues, all tests.make test-integration—TestQuickstart_*(builds & execs the real binary) pass; the search/verify assertions check output shape (bounded line count, name/footer presence), which the tabwriter change preserves.internal/sourceguardguard proven to catch injected smart quotes; thePostToolUsehook proven to fire and block.Closes #21.